#### PREPARSER AND MATH FUNCTION DEFINITIONS ####

#Immediate TODO's:
#   1) In sumseq, fix it so Increment is able to be a decimal. Don't let Finish
#      be > Start, or Increment be non-positive (or can it be negative?).
#   2) defderiv(2*x^21,x,3) doesn't work -> fix it.
#   3) newton() is screwed up.  Figure out why and fix it.
#   4) If there is an error, put it in the answer box. Don't just print the error to the console.
#   5) Comment ReduceRadical, IsPrime, Quadratic, fofx, defint.
#   6) Fix newton so that it doesn't split when we have '==' (perhaps replace == with @
#      before conversion, then replace it back after).
#   7) fofx() (and all derived functions, such as defderiv, defint) will not work if the variable 
#      was previously set to a value.
#   8) Validation: Expression cannot have double-operators.  The only ones allowed are 
#      e.g. 3*-2, 6/-2, 4^-1, 3==-3, etc. Determine if I should allow +-, --, etc.  
#      Also if something like 3+--++-+----+-5 would be acceptable.
#   9) Integrate the pre-parsing with pythonica.

from __future__ import division
import math
import pythonica
import pythonicacore

verbose = False

#---VALIDATION
#============================================================================================================
def FindInvalidChars(Expression):
    """Returns a null string if the exression does not contain any of the
       invalid characters.  Returns the invalid character in the string
       if one is present.  Invalid characters are "'`?_@\#$;:{}[] """
    InvalidChars = "\"`'?_@\#$;:{}[]"
    for I in range(0, len(InvalidChars)):
        if Expression.find(InvalidChars[I]) > -1: return InvalidChars[I]
    return ""

def ValidParens(Expression):
    """Sees if expression has the same number of "(" as ")" """
    Opened = 0
    Closed = 0
    for I in range(0, len(Expression)):
        if Expression[I] == "(": Opened += 1
        elif Expression[I] == ")": Closed += 1
        #If at any time the closed parentheses exceeds the opened parentheses,
        #   then we know that the parentheses are used incorrectly.
        if Closed > Opened: return False
    #If the opened and closed parentheses don't match up, then the
    #   Expression is invalid.
    if Opened != Closed: return False
    #Everything passed, so we have valid parentheses use.
    return True

def HandleImplicit(Expression):
    """Implicit multiplication is assumed if a number is next to an open or 
       closed parentheis, or if a number is next to a letter.  The following
       example illustrates all three:
       3(1+5sqrt(4)-(4/2)7) ==> 3*(1+5*sqrt(4)-(4/2)*7) """
    CP = 0 #Current Position
    LenExp = len(Expression)
    while CP < LenExp - 1:
        CC = Expression[CP] #Current Character
        NC = Expression[CP+1] #Next Character
        if IsThisNumeric(CC) == True and NC =="(":
            Expression = Expression[0:CP+1] + "*" + Expression[CP+1:]
            LenExp += 1
        elif IsThisNumeric(CC) == True and IsThisAlpha(NC) == True:
            Expression = Expression[0:CP+1] + "*" + Expression[CP+1:]
            LenExp += 1
        elif CC == ")" and IsThisAlpha(NC) == True:
            Expression = Expression[0:CP+1] + "*" + Expression[CP+1:]
            LenExp += 1
        CP += 1
    return Expression

def Convert2Pythonica(Expression):
    Expression = FixPythonicaParens(Expression) #Puts brackets at functions
    Expression = FixPythonicaMulDivExpon(Expression, "^") #--\
    Expression = FixPythonicaMulDivExpon(Expression, "*") #-- | Fixes problems like 3*-2 (three times negative two) 
    Expression = FixPythonicaMulDivExpon(Expression, "/") #--/
    if verbose: print "Convert2Pythonica returning: " + Expression + "\n __________"
    return Expression

def FixPythonicaParens(Expression):
    """Functions should be surrounded by [] for pythonica, e.g. (2+1)*log(4*(3+1),2) should
       be (2+1)*log[4*(3+1),2] ."""
    CP = 1
    BracksAt = []
    LenExp = len(Expression)
    #Every time there is a "(" after a letter, we know
    #   it is a function.  Convert the "(" to a "["
    while CP < LenExp:
        CC = Expression[CP]
        PC = Expression[CP-1]
        if CC == "(" and IsThisAlpha(PC) == True:
            Expression = Expression[0:CP] + "[" + Expression[CP+1:]
            BracksAt.append(CP)
        CP += 1
    if verbose: print "FixPythonicaParens fixing: " + Expression
    #Find all the "["'s matching ")" and convert it to a "]"
    for Bracket in BracksAt:
        OParens = 0
        CParens = 0
        CP = Bracket + 1
        while OParens >= CParens and CP < LenExp:
            CC = Expression[CP]
            if CC == "(": OParens += 1
            elif CC == "[": OParens += 1
            elif CC == ")": CParens += 1
            CP += 1
        if CParens > OParens:
            Expression = Expression[0:CP-1] + "]" + Expression[CP:]
    if verbose: print "FixPythonicaParens returning: " + Expression
    return Expression

def FixPythonicaMulDivExpon(Expression, Op):
    """This fixes when a + or a - sign comes after the given Op.
       It does this by surrounding the number after Op with parentheses."""
    if verbose: print "MulDivExpon  IN for " + Op + " : " + Expression
    CP = 0
    LenExp = len(Expression)
    Occurrences = []
    for CP in range(0, LenExp-1):
        CC = Expression[CP]
        NC = Expression[CP+1]
        if CC == Op and (NC == "+" or NC == "-"): Occurrences.append(CP)
    Times = len(Occurrences) #Number of occurrences
    if verbose: print "Occurrences: " + pythonica.tostr(Occurrences)
    for I in range(0, Times):
        MinusStart = False
        ExponentEnd = False
        CP = Occurrences[I] + 1 #Current position is directly after the occurence.
        CC = Expression[CP]
        if CC == "-": MinusStart = True
        Expression = Expression[0:CP] + "(" + Expression[CP:]
        LenExp += 1
        CP += 2 #go to the character after the + or -
        FoundEnd = False
        while FoundEnd == False:
            CC = Expression[CP]
            if CP == LenExp - 1:
                #If we are at the end and still haven't found a place for
                #   the closing exponent, put it at the end
                Expression = Expression + ")"
                LenExp += 1
                FoundEnd = True
            elif IsOperator(CC) == True or CC == ")" or CC == "]":
                if CC == "^": ExponentEnd = True
                Expression = Expression[0:CP] + ")" + Expression[CP:]
                LenExp += 1
                FoundEnd = True
            CP += 1
        if I != Times - 1: 
            for J in range(I+1, Times): Occurrences[J] += 2
        if MinusStart and ExponentEnd and Op != "^":
            #If we have something like -4*-2^2, the negative sign should
            #   not be encapsulated in the parenthesis if the exponent is
            #   even, or we'll have a wrong answer.  So we need to loop
            #   backwards and change the sign before it to the opposite.
            #   -4*(-2)^2 should evaluate to -16, but -4*-2^2 should evaluate
            #   to 16.
            Strung = ""
            #We're at the exponent sign under all circumstances, so we'll
            #   take the next character at the beginning:
            while not CanParse(Strung):
                CP += 1
                CC = Expression[CP]
                #Strung will build up until we have a number.
                Strung = Strung + CC
                if verbose: print str(CP) + " :: Strung = " + Strung
            A = pythonica.unparse(pythonica.handleInput(Strung)[1])
            if float(A)/2 == float(int(float(A)/2)): #if the exponent is even:
                CP = Occurrences[I]
                while not (CC == "+" or CC == "-" or CP == 0): #Loop back until we hit a + or - or the beginning
                    CP -= 1
                    CC = Expression[CP]
                    if verbose: print CC + " :: CC at : " + str(CP)
                    if CC == ")":
                        CParens = 1
                        while CParens != 0:
                            CP -= 1
                            CC = Expression[CP]
                            if CC == "(": CParens -= 1
                            elif CC == ")": CParens += 1
                        #Set the variables to the current position
                        if CP != 0: CP -= 1
                        CC = Expression[CP]
                if CC == "+" or CC == "-":
                    if verbose: print "Found + or -"
                    #Give it the opposite sign to make up for
                    #   the encapsulated negative in even exponent.
                    if CC == "+": Expression = Expression[0:CP] + "-" + Expression[CP+1:]
                    else: Expression = Expression[0:CP] + "+" + Expression[CP+1:]
                elif CP == 0:
                    if verbose: print "AT BEGINNING"
                    #Well, if we didn't find a + or - and we hit
                    #   the beginning of the string, we add the
                    #   negative sign in front.
                    Expression = "-" + Expression
                    LenExp += 1
                    if I != Times - 1: 
                        #Adding the - sign shifts everything by one
                        for J in range(I+1, Times): Occurrences[J] += 1
    if verbose: print "MulDivExpon OUT: " + Expression
    return Expression

#---TESTING ...
#==============================================================================================================
def IsThisAlpha(Char):
    """Character is a letter if its uppercase does not match its lowercase."""
    if Char.upper() != Char.lower(): return True
    return False

def IsThisNumeric(Char):
    """Character is a number if it is one of 1234567890"""
    Nums = "1234567890"
    return (Nums.find(Char) > -1)

def IsOperator(Char):
    """Character is an operator if it is one of +-/*^|=~><! """
    Ops = "+-/*^|=~><!" #Add % sign if it turns out we don't need its current functionality in pythonica.
    return (Ops.find(Char) > -1)

def CanParse(Expression):
    if Expression == "": return False
    try:
        A = pythonica.handleInput(Expression)
        #No errors, so return True, but if we have errors, it can't parse, so
        #   return False.
        return True
    except: return False

#---MATH FUNCTIONS
#==============================================================================================================
#DegreeMode is set by the check box on the app window. See MainFrame for action.
DegreeMode = False
#   DO NOT USE NUMBERS IN FUNCTION NAMES!
#   Otherwise, it will be interpreted as implicit multiplication.
##==============I don't think I need any of the code in doulbe comments:=======
##def abs(num):
##    return math.fabs(num)
##def sqrt(num):
##    return math.sqrt(num)
##def nroot(num1, num2):
##    #Returns nth root of a number.  For instance, nroot(125,3) would take
##    #   the cube root of 125 by making the statement 125**(1/3)
##    return num1**(1.0/num2)
def sin(num):
    if DegreeMode == False: 
        return math.sin(num)
    else: 
        #If DegreeMode is on, returns answer in degrees.
        return math.sin(num*math.pi/180)
def cos(num):
    if DegreeMode == False: return math.cos(num)
    else: 
        #For some reason, cos(90) in degrees evaluates to a wierd answer.
        #   I corrected that here.
        if num == 90: return 0.0
        return math.cos(num*math.pi/180)
def tan(num):
    if DegreeMode == False: return math.tan(num)
    else: return math.tan(num*math.pi/180)
def csc(num):
    if DegreeMode == False: return 1.0/math.sin(num)
    else: return 1.0/math.sin(num*math.pi/180)
def sec(num):
    if DegreeMode == False: return 1.0/math.cos(num)
    else: return 1.0/math.cos(num*math.pi/180)
def cot(num):
    return 1.0/math.tan(num*math.pi/180)
def asin(num):
    if DegreeMode == False: return math.asin(num)
    else: return math.asin(num)*180/math.pi
def acos(num):
    if DegreeMode == False: return math.acos(num)
    else: return math.acos(num)*180/math.pi
def atan(num):
    if DegreeMode == False: return math.atan(num)
    else: return math.atan(num)*180/math.pi
##def sinh(num):
##    #sinh apparently returns the same number in Degree and Radian mode.
##    return math.sinh(num)
##def cosh(num):
##    return math.cosh(num)
##def tanh(num):
##    return math.tanh(num)
##def ln(num):
##    return math.log(num, math.e) 
##def log(num1, num2=10.0):
##    return math.log(num1, num2)
def factorial(n):
    if n == 0:
        #0! is defined as 1
        return 1
    elif n < 0 or float(int(n)) != float(n):
        raise ValueError, "In factorial, n must be a non-negative integer"
    else:
        Accumulator = 1
        #convert n to an integer, in case they put something like 2.0 for n
        n = int(n)
        #Count down from n to 1 and multiply the Accumulator each time by I:
        #   5! = 5*4*3*2*1, but I left the 1 off, since it's not necessairy.
        for I in range(n, 1, -1):
            Accumulator = Accumulator * I
        return float(Accumulator)
def npr(n, r):
    if n < 1 or r < 0 or float(int(n)) != float(n) or float(int(r)) != float(r) or r > n:
        raise ValueError, "In nPr, n and r must be positive integers, where n > r"
    elif float(r) == 0.0:
        #I'm pretty sure this is right.  I'll have to check.
        return 0
    else:
        Permutation = 1
        r = int(r)
        #nPr is like n!, except instead of multiplying down to 1, you multiply down r times:
        #   nPr(5,2) = 5*4
        #   nPr(100,4) = 100*99*98*97
        #   nPr(10,10) = 10!
        for I in range(1, r+1):
            Permutation = Permutation * n
            n = n - 1
        return float(Permutation)
def ncr(n, r):
    if n < 1 or r < 0 or float(int(n)) != float(n) or float(int(r)) != float(r) or r > n:
        raise ValueError, "In nCr, niether n nor r can be negative. Only r can be zero. Both must be integers."
    else:
        n = int(n)
        r = int(r)
        nFactorial = 1
        #Calculate n!
        for I in range(1, n+1):
            nFactorial = nFactorial * I
        #Calculate (n-r)!
        nMinusRFactorial = 1
        for I in range(1, n-r+1):
            nMinusRFactorial = nMinusRFactorial * I
        #Calculate r!
        rFactorial = 1
        for I in range(1, r+1):
            rFactorial = rFactorial * I
        #nCr = n!/((n-r)! * r!)
        Combination = nFactorial/(nMinusRFactorial * rFactorial)
        return float(Combination)
def fofx(Expression, Variable, VarVal):
    #Pythonica gives me back a string with spaces in it...
    Expression = "".join(Expression.split())
    #If the VarVal has e notation in it, get rid of it so pythonica doesn't think
    #   it's a symbol.
    StrVarVal = str(VarVal)
    if StrVarVal.find("e", 0, len(StrVarVal)) > -1: StrVarVal = StrVarVal.replace("e", "*10^")
    StrVarVal = Convert2Pythonica(StrVarVal)
    CP = 0 #Current Position
    LenExp = len(Expression)
    while CP < LenExp:
        CC = Expression[CP] #Current Character
        if CP != LenExp-1: NC = Expression[CP+1] #Next Character
        if CP != 0: PC = Expression[CP-1] #Previous Character
        if CP == 0:
            if Expression == Variable: Expression = StrVarVal
            elif CC == Variable and IsOperator(NC) == True:
                Expression = StrVarVal + Expression[CP+1:]
                LenExp += len(StrVarVal) - 1
        elif CP == LenExp-1:
            if CC == Variable:
                Expression = Expression[0:CP] + StrVarVal
        else:
            if IsThisAlpha(PC) == False and IsThisAlpha(NC) == False and CC == Variable:
                Expression = Expression[0:CP] + StrVarVal + Expression[CP+1:]
                LenExp += len(StrVarVal) - 1
        CP += 1
    Answer = pythonica.unparse(pythonica.handleInput(Expression)[1])
    return float(Answer)
def sumseq(Equation, Variable, Start, Finish, Increment=1):
    Equation = "".join(Equation.split())
    Sum = 0
    #Calculates sum of a sequence.
    #   e.g. sumseq(x,x,1,7,1) will evaluate 1+2+3+4+5+6+7.
    #   sumseq(2x,x,1,3,0.5) should = 2*1 + 2*1.5 + 2*2 + 2*2.5 + 2*3
    #   Increment needs to be able to be a decimal.  Fix this.  Somehow. 
    for I in range(Start, Finish+1, Increment):
        Sum = Sum + fofx(Equation, Variable, I)
    return float(Sum)
def defint(Equation, Variable, a, b, n=200):
    Equation = "".join(Equation.split())
    #I knew at the time I wrote this how integrals work, but I've forgotten,
    #   so I can't really comment this...
    DeltaX = (b - a)/(2 * n)
    Sum = fofx(Equation, Variable, a) + fofx(Equation, Variable, b)
    Coefficient = 2
    for I in range(1, 2*n):
        #I do remember this uses Simpson's rule: Coefficient alternates between 4 and 2
        Coefficient = 6 - Coefficient
        XsubK = a + I * DeltaX
        Sum = Sum + Coefficient * fofx(Equation, Variable, XsubK)
    DefIntegral = (1.0/3.0) * DeltaX * Sum
    return DefIntegral
def defderiv(Equation, Variable, VarVal, H=0.00000001):
    Equation = "".join(Equation.split())
    #H is a number close to zero.  One definition of the derivative is the
    #   limit of h to zero of (f(x+h)-f(x))/h
    #   I'm not sure if this is the most effective definition.  It seems to
    #   be slightly inaccurate.
    #x+h = VarVal + H
    XpH = VarVal + H
    #f(x)
    FX = fofx(Equation, Variable, VarVal)
    #f(x+h)
    FXpH = fofx(Equation, Variable, XpH)
    #find the approximation.
    ApproxDeriv = (FXpH - FX) / H
    return ApproxDeriv
def newton(Expression, Variable, Estimate, NumLoops=51):
    #Something's wrong with this.  newton(4x+2,x,34) returns -6, and 
    #   newton(2x+2+2x,x,34) returns -3, and they're both wrong.
    Expression = "".join(Expression.split())
    ConnectString = Expression
    if Expression.find("=", 0, len(Expression)) > -1:
        #If someone has newton(2x+3=4,x,40), then I split at the equal sign
        #   and do this: (2x+3)-(4) so that the equation equals zero and Newton's
        #   theory can be put to use.  Otherwise we assume an expression like
        #   "x**2-3" is equal to zero already.
        ##THIS METHOD WILL NOT WORK IF SOMEONE HAS == IN THEIR EXPRESSION.
        ##   FIX THAT!  Then again, I don't think == will be able to go into the expression.
        BrokeString = Expression.split("=")
        BrokeString[0] = "(" + BrokeString[0] + ")"
        BrokeString[1] = "(" + BrokeString[1] + ")"
        ConnectString = BrokeString[0] + "-" + BrokeString[1]
    for I in range(1, NumLoops):
        FX = fofx(ConnectString, Variable, Estimate)
        Deriv = defderiv(ConnectString, Variable, Estimate)
        #The more times you loop, the more accurate the answer becomes.
        Estimate = Estimate - FX / Deriv
    return Estimate

#---FOR USE IN THE MATH BUTTON
#==============================================================================================================
def ReduceRadical(Radicand):
    Counter = 1.0
    SqrtRad = math.sqrt(Radicand)
    Accumulator = 1.0
    while Counter <= SqrtRad:
        Counter = Counter + 1
        if Radicand/(Counter**2) == int(Radicand/(Counter**2)):
            Accumulator = Accumulator * Counter
            Radicand = Radicand / (Counter**2)
            SqrtRad = math.sqrt(Radicand)
            Counter = 1
    Result = [int(Accumulator), int(Radicand)]
    return Result
def IsPrime(Num):
    SqrtNum = math.sqrt(Num)
    Prime = True
    Counter = 1
    while Counter <= SqrtNum and Prime == True:
        Counter = Counter + 1
        if int(Num/float(Counter)) == Num/float(Counter): Prime = False
    if Num == 2: Prime = True
    Result = [Prime, Counter]
    return Result
def GCF(A, B):
    #Uses the Euclidean Algorithm to compute the Greatest Common Factor
    while B != 0:
        Z = B
        B = A % B
        A = Z
    return A
def LCD(A, B):
    #Equation for Lowest Common Denominator:
    return A*B/GCF(A, B)
def Quadratic(A, B, C):
    if B**2 - 4*A*C >= 0:
        IsImaginary = False
        Quad = [IsImaginary,
                ((-B) + math.sqrt(B**2 - 4*A*C))/(2*A),
                ((-B) - math.sqrt(B**2 - 4*A*C))/(2*A)]
        return Quad
    else:
        return QuadraticImg(A, B, C)
def QuadraticImg(A, B, C):
    Denominator = str(2*A)    
    Determinate = 4*A*C - B**2
    if float(int(Determinate)) == float(Determinate): ReducRad = ReduceRadical(Determinate)
    else: ReducRad = [1, Determinate]
    if float(int(B)) == float(B):
        X = GCF(ReducRad[0], B)
        Y = GCF(X, 2*A)
        A = A/Y
        B = B/Y
        ReducRad[0] = ReducRad[0]/Y
    # \u00b1 is +- in unicode
    if Determinate == 1:
        Numerator = str(-B) + u" \u00b1 i"
    else:
        if ReducRad[0] == 1:
            if math.fabs(2*A) == math.fabs(B): 
                B = int(B/math.fabs(2*A))
                Denominator = int(2*A/math.fabs(2*A))
            Numerator = str(-B) + u" \u00b1 i*sqrt(" + str(ReducRad[1]) + ")"
        elif ReducRad[1] == 1:
            Numerator = str(-B) + u" \u00b1 " + str(ReducRad[0]) + "*i"
        else:
            Numerator = str(-B) + u" \u00b1 " + str(ReducRad[0]) + \
                        "*i*sqrt(" + str(ReducRad[1]) + ")"
    IsImaginary = True
    Quad = [IsImaginary, unicode(Numerator), unicode(Denominator)]
    return Quad

#=============================================================================================================
#---EVALUATES EXPRESSION--------------------------------------------------------------------------------------
#=============================================================================================================
def Evaluate(Expression):
    Expression = "".join(Expression.split()) #Delete whitespace
    if Expression == "": return ""
    Expression = Expression.lower()
    Expression = Expression.replace("**", "^") #Some (coughpythoncough) people use ** to mean ^
    Expression = HandleImplicit(Expression)
    Expression = Expression.replace("<>", "!=")
    InvChar = FindInvalidChars(Expression)
    if InvChar != "":
        return "\"" + InvChar + "\" is an invalid character."
    elif ValidParens(Expression) == False:
        return "Invalid Use Of Parentheses"
    elif IsOperator(Expression[len(Expression)-1]): return "Expresson cannot end with an operator"
    else:
        Expression = Convert2Pythonica(Expression)
        Answer = pythonica.unparse(pythonica.handleInput(Expression)[1])  #Help! Help! I'm being expressed!
        try:
            pythonicacore.ExprSet([pythonicacore.ExprSymbol("ans"), pythonicacore.ExprNumber(float(Answer))]).Eval()
            return Answer
        except: return Answer
